﻿using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using YFurion.Extensions;

namespace YFurion.Linq.Extensions
{
    public static class ExpressionCombiner
    {
        public static Expression<Func<T, bool>> CreateExpression<T>(this string propertyName, string compareMethod, object compareValue)
        {
            if (propertyName.IsNullOrWhiteSpace()
                || compareMethod.IsNullOrWhiteSpace()
                || compareValue == null)
            {
                return null;
            }

            Type entityType = typeof(T);
            PropertyInfo propertyInfo = entityType.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
            if (propertyInfo == null)
            {
                return null;
            }

            ParameterExpression parameterExpression = Expression.Parameter(entityType);
            MemberExpression propertyExpression = Expression.Property(parameterExpression, propertyName);
            ConstantExpression compareValueExpression = Expression.Constant(compareValue);
            Type propertyType = propertyInfo.PropertyType;
            Expression body = null;
            switch (compareMethod)
            {
                case "In":
                case "NotIn":
                    MethodInfo methodInfo1 = typeof(Enumerable).GetMethods().FirstOrDefault(m => m.Name.Equals("Contains") && m.GetParameters().Length == 2);
                    MethodInfo methodInfo2 = methodInfo1.MakeGenericMethod(propertyType);
                    MethodCallExpression methodCallExpression = Expression.Call(
                        methodInfo2,
                        compareValueExpression,
                        propertyExpression);
                    body = compareMethod.Equals("In") ? methodCallExpression : Expression.Not(methodCallExpression);
                    break;
                case "Contains":
                case "StartsWith":
                case "EndsWith":
                    body = Expression.Call(
                    propertyExpression,
                    propertyType.GetMethod(compareMethod, new Type[] { propertyType }),
                    compareValueExpression);
                    break;
                case "==":
                    body = Expression.Equal(propertyExpression, compareValueExpression);
                    break;
                case "!=":
                    body = Expression.NotEqual(propertyExpression, compareValueExpression);
                    break;
                case ">":
                    body = Expression.GreaterThan(propertyExpression, compareValueExpression);
                    break;
                case ">=":
                    body = Expression.GreaterThanOrEqual(propertyExpression, compareValueExpression);
                    break;
                case "<":
                    body = Expression.LessThan(propertyExpression, compareValueExpression);
                    break;
                case "<=":
                    body = Expression.LessThanOrEqual(propertyExpression, compareValueExpression);
                    break;
            }
            Expression<Func<T, bool>> expression = body == null ? null : Expression.Lambda<Func<T, bool>>(body, parameterExpression);
            return expression;
        }

        public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
        {
            Expression<Func<T, bool>> expression = expression1.AndOr(expression2, true);
            return expression;
        }

        public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
        {
            Expression<Func<T, bool>> expression = expression1.AndOr(expression2, false);
            return expression;
        }

        private static Expression<Func<T, bool>> AndOr<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2, bool isAnd)
        {
            (bool can, Expression<Func<T, bool>> expression) = expression1.Default(expression2);
            if (!can)
            {
                return expression;
            }

            var parameter = Expression.Parameter(typeof(T));

            var leftVisitor = new ReplaceExpressionVisitor(expression1.Parameters[0], parameter);
            var left = leftVisitor.Visit(expression1.Body);

            var rightVisitor = new ReplaceExpressionVisitor(expression2.Parameters[0], parameter);
            var right = rightVisitor.Visit(expression2.Body);

            expression = isAnd ? Expression.Lambda<Func<T, bool>>(Expression.AndAlso(left, right), parameter)
                : Expression.Lambda<Func<T, bool>>(Expression.OrElse(left, right), parameter);
            return expression;
        }

        private static (bool, Expression<Func<T, bool>>) Default<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
        {
            if (expression1 == null && expression2 == null)
            {
                return (false, null);
            }

            if (expression1 == null)
            {
                return (false, expression2);
            }

            if (expression2 == null)
            {
                return (false, expression1);
            }

            return (true, null);
        }

        class ReplaceExpressionVisitor : ExpressionVisitor
        {
            private readonly Expression _oldValue;
            private readonly Expression _newValue;

            public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)
            {
                _oldValue = oldValue;
                _newValue = newValue;
            }

            public override Expression Visit(Expression node)
            {
                if (node == _oldValue)
                {
                    return _newValue;
                }

                return base.Visit(node);
            }
        }
    }
}
